package org.egrit;

import java.awt.Point;
import java.util.List;

import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.Lineal;
import com.vividsolutions.jts.geom.MultiPolygon;
import com.vividsolutions.jts.geom.Polygon;
import com.vividsolutions.jts.geom.Polygonal;
import com.vividsolutions.jts.geom.Puntal;
import com.vividsolutions.jts.geom.prep.PreparedGeometry;
import com.vividsolutions.jts.geom.prep.PreparedGeometryFactory;
import com.vividsolutions.jts.operation.union.UnaryUnionOp;
import org.egrit.geometry.GeometryUtils;
import org.egrit.geometry.PointCoordinateSequence;
import org.jaitools.CollectionFactory;
import org.jaitools.numeric.CompareOp;

/**
 * Iterates over cell positions within a given Geometry. A cell is included
 * if its centre lies within the Geometry. For Polygons and MultiPolygons
 * this is straightforward. For LineStrings, a cell is included if its
 * centre lies within half cell width of the line. For Points, the cell
 * containing the point is included.
 * 
 * <p>
 * The iterator only stores a weak reference to the Geometry.
 * 
 * @author michael
 */
public class GridGeometryIterator implements IGridIterator {
    private final DelegatePolygonIterator delegate;
    
    
    /**
     * Creates an iterator for cells that lie within for (polygonal geometries) 
     * or are intersected by (for lineal and point geometries) the given geometry.
     * 
     * @param geom the geometry
     */
    public GridGeometryIterator(Geometry geom) {
        this(geom, null);
    }
    
    /**
     * Creates an iterator for cells that lie within for (polygonal geometries) 
     * or are intersected by (for lineal and point geometries) the given geometry
     * and are within the specified bounds.
     * 
     * @param geom the geometry
     * @param bounds grid bounds
     */
    public GridGeometryIterator(Geometry geom, IGridBounds bounds) {

        if (geom instanceof Polygonal) {
            this.delegate = new DelegatePolygonIterator(geom, bounds);
            
        } else if (geom instanceof Lineal) {
            this.delegate = createFromLines(geom, bounds);
            
        } else if (geom instanceof Puntal) {
            this.delegate = createFromPoints(geom, bounds);
            
        } else {
            throw new UnsupportedOperationException(
                    geom.getGeometryType() + " is not supported");
        }
    }
    
    /**
     * Helper for constructor that creates a delegate polygon iterator 
     * from a LineString or MultiLineString object.
     * 
     * @param geom reference geometry
     * @return new delegate iterator
     */
    private DelegatePolygonIterator createFromLines(Geometry geom, IGridBounds bounds) {
        final int N = geom.getNumGeometries();
        List<Geometry> lineBuffers = CollectionFactory.list();

        for (int i = 0; i < N; i++) {
            Geometry line = geom.getGeometryN(i);
            lineBuffers.add( line.buffer(Math.sqrt(0.5) + CompareOp.DTOL) );
        }
        
        Geometry union = UnaryUnionOp.union(lineBuffers);
        if (!(union instanceof Polygonal)) {
            throw new IllegalStateException("Buffered lines do not form a Polygon or MultiPolygon");
        }

        return new DelegatePolygonIterator(union, bounds);
    }

    /**
     * Helper for constructor that creates a delegate polygon iterator 
     * from a Point or MultiPoint object.
     * 
     * @param geom reference geometry
     * @return new delegate iterator
     */
    private DelegatePolygonIterator createFromPoints(Geometry geom, IGridBounds bounds) {
        final int N = geom.getNumGeometries();
        Polygon[] polys = new Polygon[N];

        for (int i = 0; i < N; i++) {
            Coordinate c = geom.getGeometryN(i).getCoordinate();
            polys[i] = GridUtils.createCellPolygon((int) c.x, (int) c.y);
        }

        MultiPolygon mp = GeometryUtils.getGeometryFactory().createMultiPolygon(polys);
        return new DelegatePolygonIterator(mp, bounds);
    }

    @Override
    public boolean hasNext() {
        return delegate.hasNext();
    }

    @Override
    public boolean next() {
        return delegate.next();
    }

    @Override
    public Point getPos() {
        return delegate.getPos();
    }

    @Override
    public boolean isEmpty() {
        return delegate.isEmpty();
    }

    @Override
    public void reset() {
        delegate.reset();
    }

    @Override
    public IGridBounds getGridBounds() {
        return delegate.getGridBounds();
    }


    private static class DelegatePolygonIterator extends AbstractConditionalGridBoundsIterator {
        
        private final PreparedGeometry prepGeom;
        private final PointCoordinateSequence pointCS;
        private final Geometry testPoint;

        public DelegatePolygonIterator(Geometry poly, IGridBounds bounds) {
            super(bounds == null ? GridBoundsFactory.create(poly) : bounds);
            
            this.prepGeom = PreparedGeometryFactory.prepare(poly);
            this.pointCS = new PointCoordinateSequence();
            this.testPoint = new GeometryFactory().createPoint(pointCS);
        }

        @Override
        protected boolean testInclusion(Point candidatePos) {
            pointCS.setXY(candidatePos.x + 0.5, candidatePos.y + 0.5);
            testPoint.geometryChanged();
            return prepGeom.contains(testPoint);
        }
        
    }

}
